/*!
* OpenUI5
 * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company.
 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/

// Provides control sap.m.Text
sap.ui.define([
	'./library',
	'sap/ui/core/Core',
	'sap/ui/core/Control',
	'sap/ui/core/library',
	'sap/ui/Device',
	'sap/m/HyphenationSupport',
	"./TextRenderer"
],
function(library, Core, Control, coreLibrary, Device, HyphenationSupport, TextRenderer) {
	"use strict";

	// shortcut for sap.ui.core.TextAlign
	var TextAlign = coreLibrary.TextAlign;

	// shortcut for sap.ui.core.TextDirection
	var TextDirection = coreLibrary.TextDirection;

	// shortcut for sap.m.WrappingType
	var WrappingType = library.WrappingType;

	/**
	 * Constructor for a new Text.
	 *
	 * @param {string} [sId] ID for the new control, generated automatically if no ID is given
	 * @param {object} [mSettings] Initial settings for the new control
	 *
	 * @class
	 * The <code>Text</code> control can be used for embedding longer text paragraphs,
	 * that need text wrapping, into your app.
	 * If the configured text value contains HTML code or script tags, those will be
	 * escaped.
	 *
	 * As of version 1.60, you can hyphenate the text with the use of the
	 * <code>wrappingType</code> property. For more information, see
	 * {@link topic:6322164936f047de941ec522b95d7b70 Text Controls Hyphenation}.
	 *
	 * <b>Note:</b> Line breaks will always be visualized except when the
	 * <code>wrapping</code> property is set to <code>false</code>. In addition, tabs and
	 * whitespace can be preserved by setting the <code>renderWhitespace</code> property
	 * to <code>true</code>.
	 *
	 * @extends sap.ui.core.Control
	 * @implements sap.ui.core.IShrinkable, sap.ui.core.IFormContent
	 *
	 * @author SAP SE
	 * @version 1.71.0
	 *
	 * @constructor
	 * @public
	 * @alias sap.m.Text
	 * @see {@link fiori:https://experience.sap.com/fiori-design-web/text/ Text}
	 * @see {@link topic:f94deb45de184a3a87850b75d610d9c0 Text}
	 * @ui5-metamodel This control/element also will be described in the UI5 (legacy) designtime metamodel
	 */
	var Text = Control.extend("sap.m.Text", /** @lends sap.m.Text.prototype */ {
		metadata: {

			interfaces: [
				"sap.ui.core.IShrinkable",
				"sap.ui.core.IFormContent",
				"sap.m.IHyphenation"
			],
			library: "sap.m",
			properties: {

				/**
				 * Determines the text to be displayed.
				 */
				text: { type: "string", defaultValue: '', bindable: "bindable" },

				/**
				 * Available options for the text direction are LTR and RTL. By default the control inherits the text direction from its parent control.
				 */
				textDirection: { type: "sap.ui.core.TextDirection", group: "Appearance", defaultValue: TextDirection.Inherit },

				/**
				 * Enables text wrapping.
				 */
				wrapping: { type: "boolean", group: "Appearance", defaultValue: true },

				/**
				 * Defines the type of text wrapping to be used (hyphenated or normal).
				 *
				 * <b>Note:</b> This property takes effect only when the <code>wrapping</code>
				 * property is set to <code>true</code>.
				 *
				 * @since 1.60
				 */
				wrappingType : {type: "sap.m.WrappingType", group : "Appearance", defaultValue : WrappingType.Normal},

				/**
				 * Sets the horizontal alignment of the text.
				 */
				textAlign: { type: "sap.ui.core.TextAlign", group: "Appearance", defaultValue: TextAlign.Begin },

				/**
				 * Sets the width of the Text control. By default, the Text control uses the full width available. Set this property to restrict the width to a custom value.
				 */
				width: { type: "sap.ui.core.CSSSize", group: "Dimension", defaultValue: null },

				/**
				 * Limits the number of lines for wrapping texts.
				 *
				 * <b>Note</b>: The multi-line overflow indicator depends on the browser line clamping support. For such browsers, this will be shown as ellipsis, for the other browsers the overflow will just be hidden.
				 * @since 1.13.2
				 */
				maxLines: { type: "int", group: "Appearance", defaultValue: null },

				/**
				 * Specifies how whitespace and tabs inside the control are handled. If true, whitespace will be preserved by the browser.
				 * Depending on wrapping property text will either only wrap on line breaks or wrap when necessary, and on line breaks.
				 *
				 * @since 1.51
				 */
				renderWhitespace: { type: "boolean", group: "Appearance", defaultValue: false }
			},

			designtime: "sap/m/designtime/Text.designtime"
		}
	});

	/**
	 * Default line height value as a number when line height is normal.
	 *
	 * This value is required during max height calculation for the browsers that do not support line clamping.
	 * It is better to define line height in CSS instead of "normal" to get consistent <code>maxLines</code> results since normal line height
	 * not only varies from browser to browser but it also varies from one font face to another and can also vary within a given face.
	 *
	 * @since 1.22
	 * @protected
	 * @type {int}
	 */
	Text.prototype.normalLineHeight = 1.2;

	/**
	 * Determines per instance whether line height should be cached or not.
	 *
	 * Default value is true.
	 *
	 * @since 1.22
	 * @protected
	 * @type {boolean}
	 */
	Text.prototype.cacheLineHeight = true;

	/**
	 * Ellipsis(...) text to indicate more text when clampText function is used.
	 *
	 * Can be overwritten with 3dots(...) if fonts do not support this UTF-8 character.
	 *
	 * @since 1.13.2
	 * @protected
	 * @type {string}
	 */
	Text.prototype.ellipsis = '...';

	/**
	 * Defines whether browser supports native line clamp or not
	 *
	 * @since 1.13.2
	 * @returns {boolean}
	 * @protected
	 * @readonly
	 * @static
	 */
	Text.hasNativeLineClamp = ("webkitLineClamp" in document.documentElement.style);

	/**
	 * To prevent from the layout thrashing of the <code>textContent</code> call, this method
	 * first tries to set the <code>nodeValue</code> of the first child if it exists.
	 *
	 * @protected
	 * @param {HTMLElement} oDomRef DOM reference of the text node container.
	 * @param {String} [sNodeValue] new Node value.
	 * @since 1.30.3
	 */
	Text.setNodeValue = function (oDomRef, sNodeValue) {
		sNodeValue = sNodeValue || "";
		var aChildNodes = oDomRef.childNodes;
		if (aChildNodes.length === 1 && aChildNodes[0].nodeType === window.Node.TEXT_NODE) {
			aChildNodes[0].nodeValue = sNodeValue;
		} else {
			oDomRef.textContent = sNodeValue;
		}
	};

	/**
	 * Gets the text.
	 *
	 * @public
	 * @param {boolean} bNormalize Indication for normalized text.
	 * @returns {string} Text value.
	 */
	Text.prototype.getText = function (bNormalize) {
		// returns the text value and normalize line-ending character for rendering
		var sText = this.getProperty("text");

		// handle line ending characters for renderer
		if (bNormalize) {
			return sText.replace(/\r\n|\n\r|\r/g, "\n");
		}

		return sText;
	};
	/**
	 * Overwrites onAfterRendering
	 *
	 * @public
	 */
	Text.prototype.onAfterRendering = function () {
		// required adaptations after rendering
		// check visible, max-lines and line-clamping support
		if (this.getVisible() &&
			this.hasMaxLines() &&
			!this.canUseNativeLineClamp()) {

				if (Core.isThemeApplied()) {
					// set max-height for maxLines support
					this.clampHeight();
				} else {
					Core.attachThemeChanged(this._handleThemeLoad, this);
				}
		}
	};

	/**
	 * Fired when the theme is loaded
	 *
	 * @private
	 */
	Text.prototype._handleThemeLoad = function() {

		// set max-height for maxLines support
		this.clampHeight();

		Core.detachThemeChanged(this._handleThemeLoad, this);
	};

	/**
	 * Determines whether max lines should be rendered or not.
	 *
	 * @protected
	 * @returns {HTMLElement|null} Max lines of the text.
	 * @since 1.22
	 */
	Text.prototype.hasMaxLines = function () {
		return (this.getWrapping() && this.getMaxLines() > 1);
	};

	/**
	 * Returns the text node container's DOM reference.
	 * This can be different from <code>getDomRef</code> when inner wrapper is needed.
	 *
	 * @protected
	 * @returns {HTMLElement|null} DOM reference of the text.
	 * @since 1.22
	 */
	Text.prototype.getTextDomRef = function () {
		if (!this.getVisible()) {
			return null;
		}

		if (this.hasMaxLines()) {
			return this.getDomRef("inner");
		}

		return this.getDomRef();
	};

	/**
	 * Decides whether the control can use native line clamp feature or not.
	 *
	 * In RTL mode native line clamp feature is not supported.
	 *
	 * @since 1.20
	 * @protected
	 * @return {Boolean}
	 */
	Text.prototype.canUseNativeLineClamp = function () {
		// has line clamp feature
		if (!Text.hasNativeLineClamp) {
			return false;
		}

		// is text direction rtl
		if (this.getTextDirection() == TextDirection.RTL) {
			return false;
		}

		// is text direction inherited as rtl
		if (this.getTextDirection() == TextDirection.Inherit && Core.getConfiguration().getRTL()) {
			return false;
		}

		return true;
	};

	/**
	 * Caches and returns the computed line height of the text.
	 *
	 * @protected
	 * @param {HTMLElement} [oDomRef] DOM reference of the text container.
	 * @returns {int} returns calculated line height
	 * @see sap.m.Text#cacheLineHeight
	 * @since 1.22
	 */
	Text.prototype.getLineHeight = function (oDomRef) {
		// return cached value if possible and available
		if (this.cacheLineHeight && this._fLineHeight) {
			return this._fLineHeight;
		}

		// check whether dom ref exist or not
		oDomRef = oDomRef || this.getTextDomRef();
		if (!oDomRef) {
			return 0;
		}

		// check line-height
		var oStyle = window.getComputedStyle(oDomRef),
			sLineHeight = oStyle.lineHeight,
			fLineHeight;

		// calculate line-height in px
		if (/px$/i.test(sLineHeight)) {
			// we can rely on calculated px line-height value
			fLineHeight = parseFloat(sLineHeight);
		} else if (/^normal$/i.test(sLineHeight)) {
			// use default value to calculate normal line-height
			fLineHeight = parseFloat(oStyle.fontSize) * this.normalLineHeight;
		} else {
			// calculate line-height with using font-size and line-height
			fLineHeight = parseFloat(oStyle.fontSize) * parseFloat(sLineHeight);
		}

		// on rasterizing the font, sub pixel line-heights are converted to integer
		// for most of the font rendering engine but this is not the case for firefox
		if (!Device.browser.firefox) {
			fLineHeight = Math.floor(fLineHeight);
		}

		// cache line height
		if (this.cacheLineHeight && fLineHeight) {
			this._fLineHeight = fLineHeight;
		}

		// return
		return fLineHeight;
	};

	/**
	 * Returns the max height according to max lines and line height calculation.
	 * This is not calculated max height!
	 *
	 * @protected
	 * @param {HTMLElement} [oDomRef] DOM reference of the text container.
	 * @returns {int} The clamp height of the text.
	 * @since 1.22
	 */
	Text.prototype.getClampHeight = function (oDomRef) {
		oDomRef = oDomRef || this.getTextDomRef();
		return this.getMaxLines() * this.getLineHeight(oDomRef);
	};

	/**
	 * Sets the max height to support <code>maxLines</code> property.
	 *
	 * @protected
	 * @param {HTMLElement} [oDomRef] DOM reference of the text container.
	 * @returns {int} Calculated max height value.
	 * @since 1.22
	 */
	Text.prototype.clampHeight = function (oDomRef) {
		oDomRef = oDomRef || this.getTextDomRef();
		if (!oDomRef) {
			return 0;
		}

		// calc the max height and set on dom
		var iMaxHeight = this.getClampHeight(oDomRef);
		if (iMaxHeight) {
			oDomRef.style.maxHeight = iMaxHeight + "px";
		}

		return iMaxHeight;
	};

	/**
	 * Clamps the wrapping text according to max lines and returns the found ellipsis position.
	 * Parameters can be used for better performance.
	 *
	 * @protected
	 * @param {HTMLElement} [oDomRef] DOM reference of the text container.
	 * @param {int} [iStartPos] Start point of the ellipsis search.
	 * @param {int} [iEndPos] End point of the ellipsis search.
	 * @returns {int|undefined} Returns found ellipsis position or undefined.
	 * @since 1.20
	 */
	Text.prototype.clampText = function (oDomRef, iStartPos, iEndPos) {
		// check DOM reference
		oDomRef = oDomRef || this.getTextDomRef();
		if (!oDomRef) {
			return;
		}

		// init
		var iEllipsisPos;
		var sText = this.getText(true);
		var iMaxHeight = this.getClampHeight(oDomRef);

		// init positions
		iStartPos = iStartPos || 0;
		iEndPos = iEndPos || sText.length;

		// update only the node value without layout thrashing
		Text.setNodeValue(oDomRef, sText.slice(0, iEndPos));

		// if text overflow
		if (oDomRef.scrollHeight > iMaxHeight) {

			// cache values
			var oStyle = oDomRef.style,
				sHeight = oStyle.height,
				sEllipsis = this.ellipsis,
				iEllipsisLen = sEllipsis.length;

			// set height during ellipsis search
			oStyle.height = iMaxHeight + "px";

			// implementing binary search to find the position of ellipsis
			// complexity O(logn) so 1024 characters text can be found within 10 steps!
			while ((iEndPos - iStartPos) > iEllipsisLen) {

				// check the middle position and update text
				iEllipsisPos = (iStartPos + iEndPos) >> 1;

				// update only the node value without layout thrashing
				Text.setNodeValue(oDomRef, sText.slice(0, iEllipsisPos - iEllipsisLen) + sEllipsis);

				// check overflow
				if (oDomRef.scrollHeight > iMaxHeight) {
					iEndPos = iEllipsisPos;
				} else {
					iStartPos = iEllipsisPos;
				}
			}

			// last check maybe we overflowed on last character
			if (oDomRef.scrollHeight > iMaxHeight && iStartPos > 0) {
				iEllipsisPos = iStartPos;
				oDomRef.textContent = sText.slice(0, iEllipsisPos - iEllipsisLen) + sEllipsis;
			}

			// reset height
			oStyle.height = sHeight;
		}

		// return the found position
		return iEllipsisPos;
	};

	/**
	 * Gets the accessibility information for the text.
	 *
	 * @protected
	 * @returns {object} Accessibility information for the text.
	 * @see sap.ui.core.Control#getAccessibilityInfo
	 */
	Text.prototype.getAccessibilityInfo = function () {
		return { description: this.getText() };
	};

	/**
	 * Gets a map of texts which should be hyphenated.
	 *
	 * @private
	 * @returns {map} The texts to be hyphenated.
	 */
	Text.prototype.getTextsToBeHyphenated = function () {
		return {
			"main": this.getText(true)
		};
	};

	/**
	 * Gets the DOM refs where the hyphenated texts should be placed.
	 *
	 * @private
	 * @returns {map|null} The elements in which the hyphenated texts should be placed
	 */
	Text.prototype.getDomRefsForHyphenatedTexts = function () {
		return {
			"main": this.getTextDomRef()
		};
	};

	// Add hyphenation to Text functionality
	HyphenationSupport.mixInto(Text.prototype);

	return Text;

});